1 /**
2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 Normal appliation mode.
7 */
8 module app_normal;
9 
10 import logger = std.experimental.logger;
11 import std.algorithm : among, map, filter, copy;
12 import std.array : empty, appender, array;
13 import std.exception : collectException;
14 
15 import miniorm : spinSql;
16 import my.path;
17 import my.set;
18 
19 import compile_db : CompileCommandDB, toCompileCommandDB, DbCompiler = Compiler,
20     CompileCommandFilter, defaultCompilerFilter, ParsedCompileCommand;
21 
22 import code_checker.cli : Config;
23 import code_checker.database : Database, TrackFile;
24 import code_checker.engine : Environment;
25 import code_checker.cache : FileStatCache, getTrackFile, isSame;
26 
27 version (unittest) {
28     import unit_threaded : shouldEqual, shouldBeTrue, UnitTestException;
29 }
30 
31 immutable compileCommandsFile = "compile_commands.json";
32 
33 int modeNormal(Config conf) {
34     auto fsm = NormalFSM(conf);
35     return fsm.run;
36 }
37 
38 private:
39 
40 /** FSM for the control flow when in normal mode.
41  */
42 struct NormalFSM {
43     enum State {
44         init_,
45         openDb,
46         /// change the working directory of the whole program
47         changeWorkDir,
48         /// check if a DB exists at the workdir location. Affects cleanup.
49         checkForDb,
50         /// if a command is registered to generate a DB run it
51         genDb,
52         /// check if the generation of a DB went OK
53         checkGenDb,
54         /// cleanup the database
55         fixDb,
56         /// check that it went OK to perform the cleanup
57         checkFixDb,
58         runRegistry,
59         cleanup,
60         done,
61     }
62 
63     struct StateData {
64         int exitStatus;
65         bool hasGenerateDbCommand;
66         bool hasCompileDbs;
67     }
68 
69     State st;
70     Config conf;
71     CompileCommandDB compileDb;
72 
73     /// If the compile_commands.json that is written to the file system should be deleted when code_checker is done.
74     bool removeCompileDb;
75 
76     /// Root directory from which the program where initially started.
77     AbsolutePath root;
78 
79     /// Exit status of used to indicate the success to the user.
80     int exitStatus;
81 
82     Database db;
83 
84     FileStatCache fcache;
85 
86     this(Config conf) {
87         this.conf = conf;
88     }
89 
90     int run() {
91         StateData d;
92         d.hasGenerateDbCommand = conf.compileDb.generateDb.length != 0;
93         d.hasCompileDbs = conf.compileDb.dbs.length != 0;
94 
95         while (st != State.done) {
96             debug logger.tracef("state: %s data: %s", st, d);
97 
98             st = next(st, d);
99             action(st);
100 
101             // sync with changed struct members as needed
102             d.exitStatus = exitStatus;
103         }
104 
105         return d.exitStatus;
106     }
107 
108     /** The next state is calculated. Only dependent on current state and state data.
109      *
110      * These clean depenencies should make it easier to reason about the flow.
111      */
112     static State next(const State curr, const StateData d) {
113         State next_ = curr;
114 
115         final switch (curr) {
116         case State.init_:
117             next_ = State.openDb;
118             break;
119         case State.openDb:
120             next_ = State.changeWorkDir;
121             break;
122         case State.changeWorkDir:
123             next_ = State.checkForDb;
124             break;
125         case State.checkForDb:
126             next_ = State.fixDb;
127             if (d.hasGenerateDbCommand)
128                 next_ = State.genDb;
129             break;
130         case State.genDb:
131             next_ = State.checkGenDb;
132             break;
133         case State.checkGenDb:
134             next_ = State.fixDb;
135             if (d.exitStatus != 0)
136                 next_ = State.cleanup;
137             break;
138         case State.fixDb:
139             next_ = State.checkFixDb;
140             break;
141         case State.checkFixDb:
142             next_ = State.runRegistry;
143             if (d.exitStatus != 0)
144                 next_ = State.cleanup;
145             break;
146         case State.runRegistry:
147             next_ = State.cleanup;
148             break;
149         case State.cleanup:
150             next_ = State.done;
151             break;
152         case State.done:
153             break;
154         }
155 
156         return next_;
157     }
158 
159     void act_openDb() {
160         import std.datetime : dur;
161         import code_checker.database;
162 
163         try {
164             db = Database.make(conf.database);
165         } catch (Exception e) {
166             logger.warning(e.msg);
167         }
168 
169         try {
170             db.compileDbTrackApi.cleanup(2.dur!"weeks");
171         } catch (Exception e) {
172         }
173     }
174 
175     void act_changeWorkDir() {
176         import std.file : getcwd, chdir;
177 
178         root = Path(getcwd).AbsolutePath;
179         if (conf.workDir != root)
180             chdir(conf.workDir);
181     }
182 
183     void act_genDb() {
184         import std.file : exists;
185         import std.process : spawnShell, wait;
186 
187         bool isUnchanged() nothrow {
188             try {
189                 if (!exists(compileCommandsFile))
190                     return false;
191                 if (conf.compileDb.generateDbDeps.empty)
192                     return true;
193                 return !isChanged(db,
194                         conf.compileDb.generateDbDeps ~ AbsolutePath(compileCommandsFile), fcache);
195             } catch (Exception e) {
196                 logger.trace(e.msg).collectException;
197             }
198             return false;
199         }
200 
201         if (isUnchanged)
202             return;
203 
204         auto res = spawnShell(conf.compileDb.generateDb).wait;
205         fcache = typeof(fcache).init; // drop cache because the update cmd may have changed a dependency
206 
207         if (res == 0) {
208             updateCompileDbTrack(db, conf.compileDb.generateDbDeps, fcache);
209         } else {
210             // the user need some helpful feedback for what failed
211             logger.errorf("Failed running the command to generate %(%s, %)", conf.compileDb.dbs);
212             logger.error("Executed the following commands:");
213             logger.error("# if this directory is wrong use --workdir", root);
214             logger.error("cd", root);
215             logger.error(conf.compileDb.generateDb);
216             exitStatus = 1;
217         }
218     }
219 
220     void act_fixDb() {
221         import std.stdio : File;
222         import std.file : exists;
223         import compile_db : fromArgCompileDb;
224 
225         compileDb = fromArgCompileDb(conf.compileDb.dbs.map!(a => cast(string) a.idup).array);
226 
227         bool isUnchanged() nothrow {
228             try {
229                 if (!exists(compileCommandsFile))
230                     return false;
231                 return !isChanged(db, conf.compileDb.dbs ~ AbsolutePath(compileCommandsFile),
232                         fcache);
233             } catch (Exception e) {
234                 logger.trace(e.msg).collectException;
235             }
236             return false;
237         }
238 
239         if (isUnchanged)
240             return;
241 
242         logger.trace("Creating a unified compile_commands.json");
243 
244         try {
245             auto compile_db = appender!string();
246             unifyCompileDb(compileDb, conf.compiler.useCompilerSystemIncludes,
247                     conf.compileDb.flagFilter, compile_db);
248             File(compileCommandsFile, "w").write(compile_db.data);
249 
250             fcache.drop(AbsolutePath(compileCommandsFile)); // do NOT use previously cached value
251             updateCompileDbTrack(db, conf.compileDb.dbs ~ AbsolutePath(compileCommandsFile), fcache);
252         } catch (Exception e) {
253             logger.errorf("Unable to process %s", compileCommandsFile);
254             logger.error(e.msg);
255             exitStatus = 1;
256         }
257     }
258 
259     void act_runRegistry() {
260         import code_checker.engine;
261         import compile_db : fromArgCompileDb, parseFlag, CompileCommandFilter;
262         import code_checker.change : dependencyAnalyze;
263         import code_checker.engine.types : TotalResult;
264 
265         auto changed = () {
266             bool[AbsolutePath] rval;
267 
268             try {
269                 foreach (v; dependencyAnalyze(db, AbsolutePath("."), fcache).byKeyValue) {
270                     rval[v.key.AbsolutePath] = v.value;
271                 }
272             } catch (Exception e) {
273             }
274             return rval;
275         }();
276 
277         Environment env;
278         env.compileDbFile = AbsolutePath(Path(compileCommandsFile));
279         env.compileDb = compileDb;
280         env.files = () {
281             if (!conf.analyzeFiles.empty)
282                 return conf.analyzeFiles.map!(a => cast(string) a).array;
283 
284             string[] rval;
285             foreach (dbFile; env.compileDb) {
286                 if (auto v = dbFile.absoluteFile in changed) {
287                     if (*v)
288                         rval ~= dbFile.absoluteFile.toString;
289                 } else {
290                     rval ~= dbFile.absoluteFile.toString;
291                 }
292             }
293             return rval;
294         }();
295 
296         env.conf = conf;
297 
298         TotalResult tres;
299         if (!env.files.empty) {
300             auto reg = makeRegistry;
301             tres = execute(env, conf.staticCode.analyzers, reg);
302         }
303         exitStatus = tres.status.among(Status.passed, Status.none) ? 0 : 1;
304 
305         spinSql!(() {
306             auto trans = db.transaction;
307             try {
308                 removeDroppedFiles(db, env, root);
309                 removeFailing(db, root, tres.failed);
310             } catch (Exception e) {
311                 logger.trace(e.msg);
312             }
313             trans.commit;
314         });
315 
316         if (!tres.success.empty) {
317             logger.trace("Saving result for ", tres.success);
318             spinSql!(() {
319                 auto trans = db.transaction;
320                 try {
321                     saveDependencies(db, env, root, tres.success, fcache);
322                     db.dependencyApi.cleanup;
323                 } catch (Exception e) {
324                     logger.trace(e.msg);
325                 }
326                 trans.commit;
327             });
328         }
329     }
330 
331     void act_cleanup() {
332         import std.file : chdir;
333 
334         chdir(root);
335     }
336 
337     /// Generate a callback for each state.
338     void action(const State st) {
339         string genCallAction() {
340             import std.format : format;
341             import std.traits : EnumMembers;
342 
343             string s;
344             s ~= "final switch(st) {";
345             static foreach (a; EnumMembers!State) {
346                 {
347                     const actfn = format("act_%s", a);
348                     static if (__traits(hasMember, NormalFSM, actfn))
349                         s ~= format("case State.%s: %s();break;", a, actfn);
350                     else {
351                         pragma(msg, __FILE__ ~ ": no callback found: " ~ actfn);
352                         s ~= format("case State.%s: break;", a);
353                     }
354                 }
355             }
356             s ~= "}";
357             return s;
358         }
359 
360         mixin(genCallAction);
361     }
362 }
363 
364 /// Unify multiple compilation databases to one json file.
365 void unifyCompileDb(AppT)(CompileCommandDB db, const DbCompiler user_compiler,
366         CompileCommandFilter flag_filter, ref AppT app) {
367     import std.ascii : newline;
368     import std.format : formattedWrite;
369     import std.path : stripExtension;
370     import std.range : put;
371     import compile_db;
372 
373     logger.trace(flag_filter);
374 
375     void writeEntry(T)(T e) {
376         auto raw_flags = () @safe {
377             import std.json : JSONValue;
378 
379             auto app = appender!(string[]);
380             //auto pflags = e.parseFlag(flag_filter);
381             app.put(e.flags.compiler);
382             e.flags.completeFlags.copy(app);
383             // add back dummy -c otherwise clang-tidy do not work.
384             // clang-tidy says "Passed" on everything.
385             ["-c", e.cmd.absoluteFile.toString].copy(app);
386             // correctly quotes interior strings as JSON requires.
387             return JSONValue(app.data).toString;
388         }();
389 
390         formattedWrite(app, `"directory": "%s",`, cast(string) e.cmd.directory);
391         formattedWrite(app, `"arguments": %s,`, raw_flags);
392 
393         if (!e.cmd.output.empty)
394             formattedWrite(app, `"output": "%s",`, cast(string) e.cmd.absoluteOutput);
395         formattedWrite(app, `"file": "%s"`, cast(string) e.cmd.absoluteFile);
396     }
397 
398     logger.trace("database ", db);
399 
400     if (db.empty)
401         return;
402     auto entries = ParsedCompileCommandRange.make(db.fileRange.parse(flag_filter)
403             .addCompiler(user_compiler).replaceCompiler(user_compiler).addSystemIncludes.array)
404         .array;
405     if (entries.empty)
406         return;
407 
408     formattedWrite(app, "[");
409 
410     bool isFirst = true;
411     foreach (e; entries) {
412         logger.trace(e);
413 
414         if (isFirst) {
415             isFirst = false;
416         } else {
417             put(app, ",");
418             put(app, newline);
419         }
420 
421         formattedWrite(app, "{");
422         writeEntry(e);
423         formattedWrite(app, "}");
424     }
425 
426     formattedWrite(app, "]");
427 }
428 
429 @(`shall quote compile_commands entries as JSON requires when the value is a string containing "`)
430 unittest {
431     import std.algorithm : canFind;
432 
433     // arrange
434     enum test_compile_db = `[
435     {
436         "directory": "dir1/dir2",
437         "arguments": [ "cc", "-c", "-DFOO=\"bar\"" ],
438         "file": "file1.cpp"
439     }
440 ]`;
441     auto db = test_compile_db.toCompileCommandDB(Path("."));
442     // act
443     auto unified = appender!string();
444     unifyCompileDb(db, DbCompiler.init,
445             CompileCommandFilter(defaultCompilerFilter.filter.dup, 0), unified);
446     // assert
447     try {
448         unified.data.canFind(`-DFOO=\"bar\"`).shouldBeTrue;
449     } catch (UnitTestException e) {
450         unified.data.shouldEqual("a trick to print the unified string when the test fail");
451     }
452 }
453 
454 Path toIncludePath(AbsolutePath f, AbsolutePath root) {
455     import std.algorithm : startsWith;
456     import std.path : relativePath, buildNormalizedPath;
457 
458     if (f.toString.startsWith(root.toString))
459         return relativePath(f, root).Path;
460     return f;
461 }
462 
463 void saveDependencies(ref Database db, Environment env, AbsolutePath root,
464         AbsolutePath[] successFiles, ref FileStatCache fcache) {
465     import code_checker.engine.compile_db : toRange;
466     import code_checker.database : DepFile;
467 
468     auto success = toSet(successFiles);
469 
470     foreach (pcmd; toRange(env).filter!(a => a.cmd.absoluteFile in success)) {
471         db.fileApi.put(toIncludePath(pcmd.cmd.absoluteFile, root),
472                 fcache.get(pcmd.cmd.absoluteFile).checksum,
473                 fcache.get(pcmd.cmd.absoluteFile).timeStamp);
474         auto deps = depScan(pcmd, root).map!(a => DepFile(toIncludePath(a,
475                 root), fcache.get(a).checksum, fcache.get(a).timeStamp)).array;
476         db.dependencyApi.set(toIncludePath(pcmd.cmd.absoluteFile, root), deps);
477     }
478 }
479 
480 AbsolutePath[] depScan(ParsedCompileCommand pcmd, AbsolutePath root) {
481     import std.stdio : File;
482     import std..string : strip, startsWith, split;
483     import my.optional;
484     import my.container.vector;
485     import code_checker.change : toAbsolutePath;
486 
487     Set!AbsolutePath found;
488     Vector!AbsolutePath que;
489     que.put(pcmd.cmd.absoluteFile);
490 
491     void updateQueue(AbsolutePath p) {
492         if (p !in found)
493             que.put(p);
494     }
495 
496     while (!que.empty) {
497         auto curr = que.back;
498         que.popBack;
499 
500         try {
501             foreach (d; File(curr).byLine
502                     .map!(a => a.strip)
503                     .filter!(a => a.startsWith("#include"))
504                     .map!(a => a.split)
505                     .filter!(a => a.length >= 2)
506                     .map!(a => a[1])
507                     .filter!(a => a.length >= 3)
508                     .map!(a => strip(a.idup)[1 .. $ - 1].Path)
509                     .map!(a => toAbsolutePath(a, pcmd.cmd.absoluteFile.dirName.AbsolutePath,
510                         pcmd.cmd.directory, pcmd.flags.includes, pcmd.flags.systemIncludes))
511                     .filter!(a => a.hasValue)
512                     .map!(a => a.orElse(AbsolutePath.init))) {
513                 updateQueue(d);
514                 found.add(d);
515             }
516         } catch (Exception e) {
517             logger.trace(e.msg);
518         }
519     }
520 
521     return found.toArray;
522 }
523 
524 void removeDroppedFiles(ref Database db, Environment env, AbsolutePath root) {
525     auto current = env.compileDb.map!(a => a.absoluteFile.toIncludePath(root)).toSet;
526     auto dbFiles = db.fileApi.getFiles.toSet;
527     foreach (removed; dbFiles.setDifference(current).toRange) {
528         db.fileApi.removeFile(removed);
529     }
530 }
531 
532 void removeFailing(ref Database db, AbsolutePath root, AbsolutePath[] failing) {
533     import std.path : relativePath, buildNormalizedPath;
534 
535     foreach (a; failing) {
536         db.fileApi.removeFile(relativePath(a, root).Path);
537     }
538 }
539 
540 bool isChanged(ref Database db, AbsolutePath[] files, ref FileStatCache fcache) nothrow {
541     foreach (a; toSet(files).toRange) {
542         try {
543             logger.trace("checking ", a);
544             const prev = db.compileDbTrackApi.get(a);
545             const res = isSame(prev, a, fcache);
546             logger.tracef(!res, "%s is %s (prev:%s curr:%s)", a, res
547                     ? "unchaged" : "changed", prev, fcache.get(a));
548             if (!res)
549                 return true;
550         } catch (Exception e) {
551             logger.trace(e.msg).collectException;
552             return true;
553         }
554     }
555     return false;
556 }
557 
558 void updateCompileDbTrack(ref Database db, AbsolutePath[] files, ref FileStatCache fcache) nothrow {
559     foreach (a; toSet(files).toRange) {
560         try {
561             auto d = fcache.get(a);
562             db.compileDbTrackApi.put(d);
563             logger.tracef("saved track data for %s %s", a, d);
564         } catch (Exception e) {
565             logger.trace(e.msg).collectException;
566         }
567     }
568 }